package cnt5106.p2p.peer.network;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.Arrays;
import java.util.Scanner;
import java.util.StringTokenizer;
import java.util.Vector;

import cnt5106.p2p.common.Utils;
import cnt5106.p2p.common.protocol.Message;
import cnt5106.p2p.common.protocol.MessageType;
import cnt5106.p2p.peer.filesystem.FileControler;

/**
 * Function
 * Handle the follow command 
 * 
 * 1. register file to the tracker(UDP)
 * 2. register to the group (UDP)
 * 3. list file on the server
 */
public class PeerClient extends Peer {

	private Scanner sc;
	private Peer peer;
	
	private DatagramSocket clientSocket;
	private InetSocketAddress trackerInfo;
	private InetSocketAddress peerServerInfo; // Sever UDP & ADDR
	// private Logger logger;

	public PeerClient(Peer peer) {
		this.peer = peer;
		// this.logger = new Logger("log.txt");
		this.trackerInfo = new InetSocketAddress(peer.trackerAddr, peer.trackerPort);
		this.peerServerInfo = new InetSocketAddress("127.0.0.1", peer.serverUDPPort);
	}
	
	public void start() throws IOException {
		try {
			clientSocket = new DatagramSocket(clientUDPPort);
			sc = new Scanner(System.in);  
			clientLoop();
		} catch (SocketException e) {
			e.printStackTrace();
		} finally {
			clientSocket.close();
		}
	}

	public void clientLoop() throws IOException {   

		String[] cmdAry;
		String cmd, filePath;
		
		switch (peer.STATE) {
		case PEER: // Default state
			cmdAry = cmdPaser();	// support command
			cmd = cmdAry[0];
			filePath = cmdAry[1];
			
			if (cmd.equals("list"))
				list();
			else if (cmd.equals("register-peer")) // File owner
				registerFile(trackerAddr, trackerPort, filePath);
			else if (cmd.equals("register-group")) {
				while (filePath.length() < 1) {
					System.out.println("Please  enter a valid file name");
					System.out.print("file name: ");
					filePath = sc.nextLine();
				}
				registerGroup(trackerAddr, trackerPort, filePath);
			}
			else if (cmd.equals("exit"))
				System.exit(0);
			else {
				System.out.println("Error: Not support command");
				clientLoop();
			}
			break;
		
		case FILEOWNER: 
			cmdAry = cmdPaser();
			cmd = cmdAry[0];
			filePath = cmdAry[1];

			if (cmd.equals("list"))
				list();
			else if (cmd.equals("upload")) {
				upload();
			} else {
				System.out.println("Error: Not support command");
				clientLoop();
			}
			break;
		
		case FILEOWNER_STOP: 
			clientSocket.close();
			break;

		case PEER_GROUP_WAITING:
			peerWait();
			break;
			
		case PEER_GROUP_SEEKING:
			peerSeek();
			break;
		
		case PEER_FINISHED:
			clientSocket.close();
			// logger.printLog();
			// logger.writeLog();
			System.out.println("Peer Client finished");
			//TODO make server end
			//System.exit(0);
			
		default:
			break; // not suppose to reach here
		}
	}
	
	private String[] cmdPaser() {
		while (true) {                             
			System.out.print("peer> ");            
			String line = sc.nextLine().trim().toLowerCase();

			while (line.length() < 1) {            
				System.out.print("peer> ");        
				line = sc.nextLine().trim().toLowerCase();
			}

			StringTokenizer st = new StringTokenizer(line);
			String cmd = st.nextToken();           
			String arg = "";
			
			if (st.hasMoreTokens()) 
				arg = st.nextToken();
		
			String[] cmdAry = {cmd, arg};
			return cmdAry;
		}
	}
	
	/*
	 * List shared file
	 */
	@SuppressWarnings("unchecked")
	private void list() throws IOException {
		Message listMsg = Message.generate(MessageType.TRACKER_LIST, null, 0, null);
		
		try {
			DatagramPacket list = new DatagramPacket(listMsg.toByte(), listMsg.toByte().length, trackerInfo);
			clientSocket.send(list);
			
			byte[] buf = new byte[150]; 
			DatagramPacket reply = new DatagramPacket(buf, buf.length);
			clientSocket.receive(reply);
			Message listReply = Message.parse(Arrays.copyOfRange(reply.getData(), 0, reply.getLength()));	

			if (listReply.getData() != null) {
				Vector<String> filenameList = (Vector<String>) listReply.getData();
				System.out.println("Shared files are " + filenameList);
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
		}
		clientLoop();
	}

	/**
	 * A file can be upload to the tracker for sharing through UDP
	 * 
	 * @param sharedFile
	 * @throws IOException 
	 */
	private void registerFile(String host, int port, String sharedFile) throws IOException {
		try {
			File file = new File(sharedFile);
			if (!file.isFile()) {
				System.out.println("File is not exists");
				clientLoop();
			}
			
			System.out.println(file.length() + " " + (int)file.length());
			
			Message registerPeerMessage = Message.generate(MessageType.TRACKER_REGISTER_PEER, sharedFile, (int)file.length(), peerServerInfo);

			DatagramPacket registerPeer = new DatagramPacket(registerPeerMessage.toByte(), registerPeerMessage.toByte().length, trackerInfo);
			clientSocket.send(registerPeer);

			byte[] buf = new byte[150]; 
			DatagramPacket reply = new DatagramPacket(buf, buf.length);
			clientSocket.receive(reply);
			Message registerPeerReply = Message.parse(reply.getData());
						
			if (registerPeerReply.getType() == MessageType.TRACKER_DUPLICATE_FILENAME)
				System.out.println(" \"" + sharedFile + "\" is already in the shared file list");

			if (registerPeerReply.getFilename().equals(sharedFile))
				System.out.println(" \"" + sharedFile + "\" has been put into shared file list"); 
		
		} catch (SocketException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			peer.STATE = PEER_STATE.FILEOWNER;
			peer.sharedFile = sharedFile;
			peer.sharedFileControler = new FileControler(sharedFile);  // use to manipulate file
			clientLoop();
		}
	}

	/**
	 * A peer can register to the sharing group to tracker through UDP
	 * @throws IOException 
	 */
	@SuppressWarnings("unchecked")
	private void registerGroup(String host, int port, String requestFile ) throws IOException {
		
		boolean isWaitingState = false;
		long requestedFileLength = 0;
		try {
			Message registerPeerMessage = Message.generate(MessageType.TRACKER_REGISTER_GROUP, requestFile, 0, peerServerInfo);
			DatagramPacket registerGroup = new DatagramPacket(registerPeerMessage.toByte(), registerPeerMessage.toByte().length, trackerInfo);
			clientSocket.send(registerGroup);
			
			byte[] buf = new byte[150]; 
			DatagramPacket reply = new DatagramPacket(buf, buf.length);
			clientSocket.receive(reply);
	
			Message registerGroupReply = Message.parse(Arrays.copyOfRange(reply.getData(), 0, reply.getLength()));
						
			if (registerGroupReply.getType() == MessageType.TRACKER_REPLY_GROUP) {
				isWaitingState = true;
				requestedFileLength = registerGroupReply.getNumber();
				
				peer.peerList = (Vector<InetSocketAddress>) registerGroupReply.getData();
				peer.peerList.remove(0);
				peer.peerList.remove(peer.peerList.size()-1);
				System.out.println("Peer's list is " + peer.peerList);
			} else {
				System.out.println(requestFile + " is not in the shared file list");
			}
		} catch (SocketException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			peer.requestedFile = requestFile;
			peer.requestedFileControler = new FileControler(requestFile, requestedFileLength);  // create a controler for the requested file
			if (isWaitingState)
				peer.STATE = PEER_STATE.PEER_GROUP_WAITING;
			clientLoop();
		}
	}

	/**
	 * File Owner starts upload file
	 * @throws IOException 
	 */
	private void upload() throws IOException {
		// process file
		int peerNum = peer.peerServer.getPeerList().size();   //  number of peers
		
		if (peerNum == 0) {
			System.out.println("No peer in the group, upload finished");
			System.exit(0);
		}
		
		int chunkSetNum = (peer.sharedFileControler.chunkNum() / peerNum) + 1;      //  number of chunks for each peer
		byte[] data = null;    //  byte array converted from msg, send to socket
		Message msg = null;    //  generated by chunk
		
		//  Used in first for loop to create TCP list
		byte[] buff = new byte[150];
		DatagramPacket sendPacket = null;
		DatagramPacket recPacket = new DatagramPacket(buff, buff.length);
		Vector<InetSocketAddress> tcpList = new Vector<InetSocketAddress>();
		
		System.out.println("Upload command");
		
		// create a list of TCP sockets to send chunks
		msg = Message.generate(MessageType.PEER_TCPPORT_REQUEST, "", 0, "");  // msg is tcpPort request message
		data = msg.toByte();
		
		for (int i=0; i<peer.peerServer.getPeerList().size(); i++){
			InetSocketAddress addr = peer.peerServer.getPeerList().get(i);
			sendPacket = new DatagramPacket(data, data.length, addr);
			clientSocket.send(sendPacket);
			
			clientSocket.receive(recPacket); 
			msg = Message.parse(recPacket.getData());  // msg is reply message
			if (msg.getType() != MessageType.PEER_TCPPORT_REPLY){  
				System.out.println("Errors occured in requesting peers' TCP ports");
				System.exit(1);
			}
			else
				tcpList.add((InetSocketAddress) msg.getData());
		}
		
		peer.peerServer.getServerUDPSocket().close();
		
		
		// start TCP connection, send chunk to different peers
		for (int i=0; i<peerNum; i++){
			MultiThreadingUploader uploader = new MultiThreadingUploader(
					i, i*chunkSetNum, ((i<peerNum-1)?((i+1)*chunkSetNum):(peer.sharedFileControler.chunkNum())), tcpList.get(i));
			uploader.start();
		}
		
		peer.STATE = PEER_STATE.FILEOWNER_STOP;
		clientLoop();
	}

	/**
	 * Peer Client starts UDP, accept group list
	 * Peer Server start TCP Server to accept connect from File Owner
	 * Change to GROUPPEER_SEEKING state when finish chunk transfer from File Owner
	 * @throws IOException 
	 */
	private void peerWait() throws IOException {
		synchronized(peer.lock){
			System.out.println("Peer starts to wait for new group member or chunk");
			try {
				peer.lock.wait();
			} catch(InterruptedException e) {
				throw new RuntimeException (e);
			} finally {
				if (peer.requestedFileControler.isFinished())
					peer.STATE = PEER_STATE.PEER_FINISHED;
				else
					peer.STATE = PEER_STATE.PEER_GROUP_SEEKING;
				System.out.println("Peer end waiting..., preparing to transmit between peers"); //////////////// just for test
				clientLoop();
			}
		}
	}

	/**
	 * Peer check its missing chunks, send chunk request through its client UDP
	 * Peer Client pick three neighbors, send request one by one
	 * Peer Server UDP receive chunks from other peers
	 * @throws IOException 
	 */
	private void peerSeek() throws IOException {
		byte[] buff = new byte[1024*101];
		byte[] dataChunk = null;
		Message msg = null;
		Message reqMsg = null;
		Message ackMsg = null;
		InetSocketAddress peerAddr = null;
		InetSocketAddress[] requestPeerList = new InetSocketAddress[3];
		DatagramPacket sendPacket = null;
		DatagramPacket recPacket = new DatagramPacket(buff, buff.length);
		Vector<Byte> tempBuffer = new Vector<Byte> ();
		
		// form the list of  peers from which request chunks
		int[] rNumArray = null;
		if (peer.peerList.size()>3)
			rNumArray = new LittleRandomGen().generate(peer.peerList.size()); 
		else {
			rNumArray = new int[peer.peerList.size()];
			for (int i=0; i<rNumArray.length; i++)
				rNumArray[i] = i;
		}
		int peerCount = (peer.peerList.size()>3) ? 3 : peer.peerList.size();
		for (int i=0; i<peerCount; i++)
			requestPeerList[i] = peer.peerList.get(rNumArray[i]);

		System.out.println("Begin to ask chunks from peers ...... ");
		
		try {
			while (!peer.requestedFileControler.isFinished()){
				reqMsg = Message.generate(MessageType.PEER_REQUEST_CHUNK, 
						peer.requestedFile, peer.requestedFileControler.emptySlot(), "");
				
				for (int i=0; i<peerCount; i++) {
					peerAddr = requestPeerList[i];
					sendPacket = new DatagramPacket(reqMsg.toByte(), (reqMsg.toByte()).length, peerAddr);
					//System.out.println(peerAddr);
					clientSocket.send(sendPacket);
					// Utils.logMessage("UDP Client", clientSocket.getLocalSocketAddress(), true, sendPacket.getSocketAddress(), reqMsg);
					// logger.addLog("UDP Client", clientSocket.getLocalSocketAddress(), true, sendPacket.getSocketAddress(), reqMsg);
					
					clientSocket.receive(recPacket);
					msg = Message.parse(Arrays.copyOfRange(recPacket.getData(), 0, recPacket.getLength()));
					// Utils.logMessage("UDP Client", clientSocket.getLocalSocketAddress(), false, recPacket.getSocketAddress(), msg);
					Utils.logMessage("UDP Client", clientSocket.getLocalSocketAddress(), false, recPacket.getSocketAddress(), msg);
					
					if (msg.getType() == MessageType.PEER_POSITIVE_REPLY){
						Utils.appendArrayToVector(tempBuffer, (byte[]) msg.toByte());
						
						for (int j=0; j<peer.subChunkNum -1; j++){
							ackMsg = Message.generate(
									MessageType.PEER_DATA_ACK, msg.getFilename(), msg.getNumber(), j+1, "");
							sendPacket = new DatagramPacket(ackMsg.toByte(),
									(ackMsg.toByte()).length, recPacket.getAddress(),recPacket.getPort());
							clientSocket.send(sendPacket);
							// Utils.logMessage("UDP Client", clientSocket.getLocalSocketAddress(), true, recPacket.getSocketAddress(), ackMsg);
							// logger.addLog("UDP Client", clientSocket.getLocalSocketAddress(), true, recPacket.getSocketAddress(), ackMsg);
							
							clientSocket.receive(recPacket);
							msg = Message.parse(Arrays.copyOfRange(recPacket.getData(), 
									0, recPacket.getLength()));
							// Utils.logMessage("UDP Client", clientSocket.getLocalSocketAddress(), false, recPacket.getSocketAddress(), msg);
							// logger.addLog("UDP Client", clientSocket.getLocalSocketAddress(), false, recPacket.getSocketAddress(), msg);
							if (msg.getType() != MessageType.PEER_POSITIVE_REPLY){
								System.out.println("Errors occured when receiving subchunks from peers.");
								System.exit(1);
							}
							Utils.appendArrayToVector(tempBuffer, (byte[]) msg.getData());
						}
						
						msg = Message.parse(Utils.unboxByteArray(tempBuffer.toArray(new Byte[0])));
						tempBuffer.clear();
						
						if (msg.getType() != MessageType.PEER_POSITIVE_REPLY){
							System.out.println("Errors occured when assembling message.");
							System.exit(1);
						} else 
							dataChunk = (byte[]) msg.getData();
						
						peer.requestedFileControler.writeFile(dataChunk, msg.getNumber());
						System.out.println("Received chunk #" + msg.getNumber() + " from peer " + peerAddr);
						break;
						
					} else if (msg.getType() == MessageType.PEER_NEGATIVE_REPLY){
						continue;
					} else {
						System.out.println("Wrong message received when requesting chunks from peers");
					}
				}
			}
			
			msg = Message.generate(
					MessageType.PEER_FINISH, peer.requestedFile, 0, peerServerInfo);
			
			for (InetSocketAddress addr : peer.peerList){
				sendPacket = new DatagramPacket(msg.toByte(), (msg.toByte()).length, addr);
				clientSocket.send(sendPacket);
			}
			
			System.out.println("Finished all chunks!" + peerServerInfo);
			
		} catch (IOException  e) {
			e.printStackTrace();
		} finally {
			peer.STATE = PEER_STATE.PEER_FINISHED;
			clientLoop();
		}
	}
	
	class MultiThreadingUploader extends Thread {
		
		int i = 0;
		int from = 0;
		int to = 0;
		byte[] chunk = null;
		byte[] ack = new byte[150];
		byte[] data = null;
		Message msg = null;
		Socket fileOwnerToPeerTCP = null;
		BufferedInputStream ackFromPeer = null;
		BufferedOutputStream dataToPeer = null;  // for sending data to peers
		
		public MultiThreadingUploader(int i, int from, int to, InetSocketAddress addr) throws IOException {
			//this.chunkSetNum = chunkSetNum;
			this.i = i;
			this.from = from;
			this.to = to;
			this.fileOwnerToPeerTCP = new Socket(addr.getAddress(), addr.getPort());
			this.ackFromPeer = new BufferedInputStream(fileOwnerToPeerTCP.getInputStream());
			this.dataToPeer = new BufferedOutputStream (
					new DataOutputStream(fileOwnerToPeerTCP.getOutputStream()));
		}
		
		public void run (){
			//System.out.println("chunkSetNum : " + chunkSetNum);
			for (int j=from; (j<to)&&(!peer.sharedFileControler.isFinished()); j++){
				try {
					chunk = peer.sharedFileControler.readFile(j, true);
				} catch (IOException e) {
					e.printStackTrace();
				}
				msg = Message.generate(MessageType.PEER_FILEOWNER_DATAPACKET, peer.sharedFile, j, chunk);  // msg is chunk data
				data = msg.toByte();
				
				try {
					dataToPeer.write(data);
				} catch (IOException e) {
					e.printStackTrace();
				}  //send data to the ith peer.
				try {
					dataToPeer.flush();
				} catch (IOException e) {
					e.printStackTrace();
				}
				System.out.println("Send chunk " + j + " to peer" + i); 
				
				//  wait for ACK from peer
				try {
					if (ackFromPeer.read(ack) == -1){
						System.out.println("Errors occured in receiving data from peer");
						System.exit(1);
					}
				} catch (IOException e) {
					e.printStackTrace();
				}
				msg = Message.parse(ack);
				if (msg.getType() != MessageType.PEER_DATA_ACK){
					System.out.println("ACK message is expected");
					System.exit(1);
				}
			}
			
			msg = Message.generate(MessageType.PEER_FILEOWNER_END, peer.sharedFile, 0, null);
			data = msg.toByte();
			
			try {
				dataToPeer.write(data);
			} catch (IOException e) {
				e.printStackTrace();
			}  // send end msg to the ith peer
			try {
				dataToPeer.flush();
			} catch (IOException e) {
				e.printStackTrace();
			}
			
			try {
				dataToPeer.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
			try {
				fileOwnerToPeerTCP.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}